在 Flutter 中,除了 Widget
、Element
、和 RenderObject
結構外,還有一個 Layer
結構。就像 Element 結構在建構階段產生 RenderObject
結構,RenderObject
結構則在渲染階段產生 Layer
結構,當 RenderObject.isRepaintBoundary
為 true 時會建立 Layer
。參考原始碼
我們用 React 來類比;
- Widget = React Component,可以組成樹狀結構.
- Element = React.element
- RenderObject = 虛擬 DOM
- Layer = 實際 DOM
也就是 Widget > Element > RenderObject > Layer。
Layer 是耗費效能的。文件說明如下:
使用 Layer 時,它們會導致 Rendering Pipeline 必須切換渲染目標(從一個 Layer 到另一個)。渲染目標的切換可能會刷新 GPU 的命令緩衝區,這表示原本可以批次處理獲得優化的效果會消失。渲染目標的切換還會產生大量記憶體需求,因為 GPU 需要將當前幀的內容從優化寫入的記憶體中局部複製出來,然後在切換回原來的 Layer 時再複製回去。
然而,通過將 UI 的部分內容隔離到獨立的 Layer 中,Flutter 可以在只有 UI 的一小部分發生變化時避免重新渲染整個畫面。Layer 有助於實現流暢的動畫效果,因為它們允許 UI 的某些部分獨立進行動畫,而不影響 UI 的其他部分。
因此,為了保持應用程式的 Layer 最佳化,請遵循以下規則:
那麼具體該如何實現:
最簡單的方式就是使用 RepaintBoundary
組件。它繼承自 SingleChildRenderObjectWidget
,表示這個組件有專屬的 RenderObject
,而這個 RenderObject
的 isRepaintBoundary
屬性設定為 true。使用 RepaintBoundary
可以輕鬆地將 UI 的某一部分隔離成獨立的 Layer。
RepaintBoundary(
child: CircularProgressIndicator() // <- 動畫組件
)
這個範例我們限制了重新渲染的範圍,否則整個繪圖層都會重新渲染。
加入 RepaintBoundary
之後:
值得注意的是預設情況下,AppBar
也會在一個獨立的 Layer 中。如果我們檢查 AppBar
的程式碼,會發現它包含了 AnnotatedRegion
,而 AnnotatedRegion
對應的 RenderObject
的 alwaysNeedsCompositing
設定為 true.
這樣設計的原因是,但我們捲動 Scaffold
的 body
時,AppBar
會固定不動,因此不會重新渲染。如果你自己客製化了自己的 AppBar
別忘了將它隔離在單獨的 Layer.
儘量避免在圖片使用 Opacity
或 ColorFiltered
組件。請使用 Image
的 color
和 blendMode
參數。更多資訊可以參考 Common mistakes with Images in Flutter。
不要在動畫中使用 Opacity
,AnimatedOpacity
的效能更好。
使用 decoration
,避免裁切 clipping
。saveLayer
在舊的裝置上特別耗費效能,因為它會建立一個螢幕外的渲染目標,切換這些渲染目標可能耗費 1ms 的時間。即使沒有 saveLayer
裁切依舊非常耗費效能,它會影響後續所有的渲染操作。若想為組件增加形狀建議使用 DecoratedBox
或 Container
的 decoration
儘量禁止裁切;有些組件預設啟用的裁切,若你確定對於佈局沒有什麼用處,例如組件永遠不會超出 Stack
那麼我們可以關閉
Stack(
clipBehavior: Clip.none,
child: ...
)
另外,請注意 ListView
預設會為每個子項目加入重新渲染邊界。如果你確定列表的子項目不會各自獨立更新,可以在建構子中加入 addRepaintBoundaries: false
來停用此功能。更多關於優化 ListView
的資訊可以參考 Common mistakes with ListViews in Flutter。
其他像 Transform
、BackdropFilter
、ShaderMask
和 Texture
這樣的組件也會建立新的 Layer 可以多加注意。